﻿using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;

namespace Shared_Mutex_Manager_Test.Locking
{
    /// <summary>
    ///     Class to simplify the process of creating various shared mutex objects
    ///     Author: Peej
    /// </summary>
    public class SharedMutexManager
    {

        #region Instance vars
        /// <summary>
        ///     Shared Lock Name Format
        /// </summary>
        private const string SHARED_MUTEX_BASE_NAME = @"Global\{{{0}-{1}}}";
        #endregion

        #region Events
        /// <summary>
        ///     Event raised when a lock is waiting for unsignaled state
        /// </summary>
        public event EventHandler<SharedMutexEventArgs> SharedMutexWaiting;

        /// <summary>
        ///     Event raised when lock is signaled (Acquired)
        /// </summary>
        public event EventHandler<SharedMutexAcquiredEventArgs> SharedMutexAcquired;

        /// <summary>
        ///     Event raised when an Abandoned Mutex Exception is raised
        /// </summary>
        public event EventHandler<SharedMutexAbandonedEventArgs> AbandonedMutexEncountered;

        /// <summary>
        ///     Event raised whenever lock is polled for ownership. 
        ///     Setting WaitAborted = True aborts the wait process
        /// </summary>
        public event EventHandler<SharedMutexWaitAbortCheckEventArgs> SharedMutexWaitAbortCheck;
        #endregion

        #region Event Invokers
        /// <summary>
        ///  Invoke SharedLockWaiting Event
        /// </summary>
        protected virtual void OnSharedMutexWaiting(string mutexName)
        {
            var handler = this.SharedMutexWaiting;
            if (handler != null) handler(this, new SharedMutexEventArgs(mutexName));
        }

        /// <summary>
        ///     Invoke OnSharedLockAcquired Event
        /// </summary>
        protected virtual void OnSharedMutexAcquired(string mutexName, bool initiallyAcquried)
        {
            var handler = this.SharedMutexAcquired;
            if (handler != null) handler(this, new SharedMutexAcquiredEventArgs(mutexName, initiallyAcquried));
        }

        /// <summary>
        ///     Invoke OnSharedLockWaitAbortCheck Event
        /// </summary>
        /// <param name="e"></param>
        protected virtual void OnSharedMutexWaitAbortCheck(SharedMutexWaitAbortCheckEventArgs e)
        {
            var handler = this.SharedMutexWaitAbortCheck;
            if (handler != null) handler(this, e);
        }

        /// <summary>
        ///     Invoke OnAbandonedMutexEncountered Event
        /// </summary>
        protected virtual void OnAbandonedMutexEncountered(string mutexName, AbandonedMutexException exception)
        {
            var handler = this.AbandonedMutexEncountered;
            if (handler != null) handler(null, new SharedMutexAbandonedEventArgs(mutexName, exception));
        }
        #endregion

        #region Public Instance Methods
        /// <summary>
        ///     Create a Shared Mutex. If its owned, poll it until we own it or user aborts, and Wait
        ///     specified wait interval between polls. Note: Consumer is responsible for disposing Mutex
        /// </summary>
        /// <param name="mutexName"></param>
        /// <param name="waitInterval"></param>
        /// <returns></returns>
        public Mutex CreateSharedMutexAndWait(string mutexName, int waitInterval = -1)
        {
            var aborted = false; //aborted cache
            Mutex mutex = null; //the shared mutex
            try
            {
                bool owned; // state of the mutex
                mutex = new Mutex(true, GetSharedMutexName(mutexName), out owned); //create a shared mutex
                if (owned)
                {
                    this.OnSharedMutexAcquired(mutexName, true); //raise event
                }
                else
                {
                    //Notify that we will wait for a shared mutex
                    this.OnSharedMutexWaiting(mutexName);

                    //block until other processes release mutex or we abort
                    while (!owned)
                    {
                        var args = new SharedMutexWaitAbortCheckEventArgs(mutexName); //create event args
                        this.OnSharedMutexWaitAbortCheck(args); //raise event
                        aborted = args.Aborted; //Cache the aborted state
                        if (aborted) break; //break out of owned loop if we aborted
                        owned = mutex.WaitOne(waitInterval); //block while we don't own the wait handle
                    }

                    //If the user did not abort, and the user was notified of another waiting process
                    //notify the user that we are ready to resume
                    if (!aborted)
                    {
                        this.OnSharedMutexAcquired(mutexName, false); //raise event
                    }
                }

                //If we don't own the mutex, notify consumer that we are now waiting
                return mutex;
            }
            catch (AbandonedMutexException e)
            {
                //Another process Abandoned our mutex, so try again
                if (mutex != null) mutex.Dispose();
                this.OnAbandonedMutexEncountered(mutexName, e);
                return this.CreateSharedMutexAndWait(mutexName, waitInterval);
            }
            catch (Exception)
            {
                if (mutex != null) mutex.Dispose();
                throw;
            }
        }

        /// <summary>
        ///     Create a shared mutex and wait until we own it.
        ///     Once acquired,  dynamic invoke an provided delegate
        /// </summary>
        /// <param name="criticalSectoin"></param>
        /// <param name="mutexName"></param>
        /// <param name="waitInterval"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public object SharedMutexInvoke(Delegate criticalSectoin, string mutexName, int waitInterval = -1, object[] args = null)
        {
            //try to create a mutex and wait until we own it or a consumer aborts the wait
            var owned = false;
            var mutex = this.CreateSharedMutexAndWait(mutexName, waitInterval);
            try
            {
                //Check if we own the mutex, if the user aborted then we won't
                this.SharedMutexAcquired += (sender, eventArgs) => owned = true;
                return owned ? null : criticalSectoin.DynamicInvoke(args);
            }
            finally
            {
                mutex.ReleaseMutex();
                mutex.Dispose();
            }
        }

        /// <summary>
        ///     Create a shared mutex and wait until we own it.
        ///     Once acquired,  dynamic invoke an provided Action
        /// </summary>
        /// <param name="criticalSectoin"></param>
        /// <param name="mutexName"></param>
        /// <param name="waitInterval"></param>
        /// <returns></returns>
        public void SharedMutexInvoke(Action criticalSectoin, string mutexName, int waitInterval = -1)
        {
            this.SharedMutexInvoke(criticalSectoin, mutexName, waitInterval, null);
        }

        /// <summary>
        ///     Create a shared mutex and wait until we own it.
        ///     Once acquired,  dynamic invoke an provided Func<typeparam name="T"></typeparam>
        /// </summary>
        /// <param name="criticalSectoin"></param>
        /// <param name="mutexName"></param>
        /// <param name="waitInterval"></param>
        /// <returns></returns>
        public T SharedMutexInvoke<T>(Func<T> criticalSectoin, string mutexName, int waitInterval = -1)
        {
            return (T)this.SharedMutexInvoke(criticalSectoin, mutexName, waitInterval, null);
        }

        /// <summary>
        ///     Create a Cross Process Shared Lock Name
        /// </summary>
        /// <param name="mutexName"></param>
        /// <returns></returns>
        public static string GetSharedMutexName(string mutexName)
        {
            var name = String.Format(SHARED_MUTEX_BASE_NAME, GetAppGuid(), mutexName);
            return name;
        }
        #endregion

        #region Private Static Methods
        /// <summary>
        ///     Get the Current Application Guid
        /// </summary>
        /// <returns></returns>
        private static string GetAppGuid()
        {
            string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly()
                .GetCustomAttributes(typeof(GuidAttribute), false)
                .GetValue(0)).Value;
            return appGuid;
        }
        #endregion
    }
}
